basic.js ➔ ... ➔ fetch   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 3
rs 10
1
/* global Net */
2
3
var timeout = require('../concurrent').timeout
4
var C = require('./_common')
5
var Q = C.Query
6
var H = C.Headers
7
var Slf4j = require('../logger').Slf4j
8
9
/**
10
 * Provides client configuration defaults.
11
 *
12
 * @returns {BasicHttpClientSettings}
13
 */
14
function getDefaults () {
15
  return {
16
    url: '',
17
    retryOnNetworkError: true,
18
    throwOnServerError: true,
19
    retryOnServerError: true,
20
    throwOnClientError: true,
21
    retryOnClientError: false,
22
    throwOnNotFound: false,
23
    retryOnNotFound: false,
24
    retries: 4,
25
    logger: {}
26
  }
27
}
28
29
/**
30
 * Internal response type, used to simplify processing path finding.
31
 *
32
 * RS is an abbreviation from ResponseStatus
33
 *
34
 * @enum
35
 * @readonly
36
 */
37
var RS = {
38
  NetworkError: 'NetworkError',
39
  ServerError: 'ServerError',
40
  ClientError: 'ClientError',
41
  NotFound: 'NotFound',
42
  Ok: 'Ok',
43
  fromCode: function (code) {
44
    if (code < 200 || !code) {
45
      return RS.NetworkError
46
    }
47
    if (code < 400) {
48
      return RS.Ok
49
    }
50
    if (code === 404) {
51
      return RS.NotFound
52
    }
53
    if (code < 500) {
54
      return RS.ClientError
55
    }
56
    return RS.ServerError
57
  }
58
}
59
60
/**
61
 * @typedef {Object} BasicHttpClientSettings
62
 *
63
 * @property {string} url
64
 * @property {boolean} retryOnNetworkError
65
 * @property {boolean} throwOnServerError
66
 * @property {boolean} retryOnServerError
67
 * @property {boolean} throwOnClientError
68
 * @property {boolean} retryOnClientError
69
 * @property {boolean} throwOnNotFound
70
 * @property {boolean} retryOnNotFound
71
 * @property {string} methodOverrideHeader
72
 * @property {HeaderBag} headers Headers to be used on every request.
73
 * @property {int} retries Maximum number of retries allowed for request.
74
 * @property {LoggerOptions} logger Logger instance or context and/or name.
75
 * @property {int} timeout
76
 */
77
78
/**
79
 * @class
80
 *
81
 * @implements IHttpClient
82
 *
83
 * @param {BasicHttpClientSettings|object} [settings]
84
 * @param {netHttpRequestAsync} [transport]
85
 */
86
function BasicHttpClient (settings, transport) {
87
  transport = transport || Net.httpRequestAsync
88
  settings = settings || {}
89
  if (typeof settings === 'string' || settings instanceof String) {
90
    settings = {url: settings}
91
  }
92
  var defaults = getDefaults()
93
  var logger = Slf4j.factory(setting('logger'), 'ama-team.voxengine-sdk.http.basic')
94
  var self = this
95
  var requests = 0
96
97
  function fetch (source, key, def) {
98
    return source[key] !== undefined ? source[key] : def
99
  }
100
101
  function setting (key, def) {
102
    return fetch(settings, key, fetch(defaults, key, def))
103
  }
104
105
  function shouldRetry (status, attempt) {
106
    // number of attempts = 1 + number of retries, so it's 'greater than' rather than 'greater or equal to'
107
    // comparison
108
    return attempt <= setting('retries') && setting('retryOn' + status, false)
109
  }
110
111
  function shouldThrow (status) {
112
    return status === RS.NetworkError || setting('throwOn' + status, false)
113
  }
114
115
  function performThrow (status, request, response) {
116
    // yes, i'm counting bytes and switch is more expensive
117
    if (status === RS.ServerError) {
118
      throw new C.ServerErrorException('Server returned erroneous response', request, response)
119
    } else if (status === RS.ClientError) {
120
      throw new C.ClientErrorException('Client has performed an invalid request', request, response)
121
    } else if (status === RS.NotFound) {
122
      throw new C.NotFoundException('Requested resource hasn\'t been found', request, response)
123
    }
124
    // get exception with specified code, otherwise use default one
125
    var ErrorClass = C.codeExceptionIndex[response.code] || C.NetworkException
126
    throw new ErrorClass(null, response.code, request, response)
127
  }
128
129
  /**
130
   * Executes HTTP request.
131
   *
132
   * @param {HttpRequest} request
133
   * @return {Promise.<(HttpResponse|Error)>}
134
   */
135
  function execute (request) {
136
    var message
137
    request.timeout = typeof request.timeout === 'number' ? request.timeout : settings.timeout
138
    if (!request.method) {
139
      message = 'Request method hasn\'t been specified'
140
      return Promise.reject(new C.InvalidConfigurationException(message))
141
    }
142
    var id = request.id = request.id || ++requests
143
    request.method = request.method.toUpperCase()
144
    if (['POST', 'GET'].indexOf(request.method) === -1 && !setting('methodOverrideHeader')) {
145
      message = 'Tried to execute non-GET/POST request without specifying methodOverrideHeader in settings'
146
      return Promise.reject(new C.InvalidConfigurationException(message))
147
    }
148
    request.query = Q.normalize(request.query)
149
    request.headers = H.normalize(request.headers)
150
    if (!request.payload) {
151
      request.payload = null
152
    }
153
    var url = request.url = setting('url') + (request.url || '')
154
    logger.debug('Executing request #{} `{} {}`', request.id, request.method, url)
155
    return executionLoop(request, 1)
156
      .then(function (response) {
157
        logger.debug('Request #{} `{} {}` got response with code {}', id,
158
          request.method, url, response.code)
159
        return response
160
      }, function (e) {
161
        logger.debug('Request #{} `{} {}` resulted in error {}', id,
162
          request.method, url, e.name)
163
        throw e
164
      })
165
  }
166
167
  function executionLoop (request, attempt) {
168
    var qs = Q.encode(request.query)
169
    var url = request.url + (qs.length > 0 ? '?' + qs : '')
170
    var opts = new Net.HttpRequestOptions()
171
    var method = ['HEAD', 'GET'].indexOf(request.method) === -1 ? 'POST' : 'GET'
172
    var headers = H.override(setting('headers', {}), request.headers)
173
    if (method !== request.method) {
174
      headers[setting('methodOverrideHeader')] = [request.method]
175
    }
176
    opts.method = method
177
    opts.postData = request.payload
178
    opts.headers = H.encode(headers)
179
    logger.trace('Executing request #{} `{} {}`, attempt #{}', request.id, request.method, url, attempt)
180
    return timeout(transport(url, opts), request.timeout).then(function (raw) {
181
      var response = {code: raw.code, headers: H.decode(raw.headers), payload: raw.text}
182
      var status = RS.fromCode(response.code)
183
      var toRetry = shouldRetry(status, attempt)
184
      var toThrow = !toRetry && shouldThrow(status)
185
      logger.trace('Request #{} `{} {}` (attempt #{}) ended with code `{}` / status `{}`, (retry: {}, throw: {})',
186
        request.id, request.method, url, attempt, response.code, status, toRetry, toThrow)
187
      if (toRetry) {
188
        return executionLoop(request, attempt + 1)
189
      }
190
      if (toThrow) {
191
        performThrow(status, request, response)
192
      }
193
      response.request = request
194
      return response
195
    })
196
  }
197
198
  function request (method, url, query, payload, headers, timeout) {
199
    return execute({url: url, method: method, headers: headers, query: query, payload: payload, timeout: timeout})
200
  }
201
202
  // noinspection JSUnusedGlobalSymbols
203
  this.execute = execute
204
  this.request = request
205
206
  var methods = ['get', 'head']
207
  methods.forEach(function (method) {
208
    self[method] = function (url, query, headers, timeout) {
209
      return request(method, url, query, null, headers, timeout)
210
    }
211
  })
212
  methods = ['post', 'put', 'patch', 'delete']
213
  methods.forEach(function (method) {
214
    self[method] = function (url, payload, headers, query, timeout) {
215
      return request(method, url, query, payload, headers, timeout)
216
    }
217
  })
218
219
  /**
220
   * Perform GET request.
221
   *
222
   * @function BasicHttpClient#get
223
   *
224
   * @param {string} url
225
   * @param {QueryBag} [query]
226
   * @param {HeaderBag} [headers]
227
   * @param {int} [timeout]
228
   *
229
   * @return {HttpResponsePromise}
230
   */
231
232
  /**
233
   * Perform HEAD request.
234
   *
235
   * @function BasicHttpClient#head
236
   *
237
   * @param {string} url
238
   * @param {QueryBag} [query]
239
   * @param {HeaderBag} [headers]
240
   * @param {int} [timeout]
241
   *
242
   * @return {HttpResponsePromise}
243
   */
244
245
  /**
246
   * Perform POST request.
247
   *
248
   * @function BasicHttpClient#post
249
   *
250
   * @param {string} url
251
   * @param {string} [payload]
252
   * @param {HeaderBag} [headers]
253
   * @param {int} [timeout]
254
   *
255
   * @return {HttpResponsePromise}
256
   */
257
258
  /**
259
   * Perform PUT request.
260
   *
261
   * @function BasicHttpClient#put
262
   *
263
   * @param {string} url
264
   * @param {string} [payload]
265
   * @param {HeaderBag} [headers]
266
   * @param {int} [timeout]
267
   *
268
   * @return {HttpResponsePromise}
269
   */
270
271
  /**
272
   * Perform PATCH request.
273
   *
274
   * @function BasicHttpClient#patch
275
   *
276
   * @param {string} url
277
   * @param {string} [payload]
278
   * @param {HeaderBag} [headers]
279
   * @param {int} [timeout]
280
   *
281
   * @return {HttpResponsePromise}
282
   */
283
284
  /**
285
   * Perform DELETE request.
286
   *
287
   * @function BasicHttpClient#delete
288
   *
289
   * @param {string} url
290
   * @param {string} [payload]
291
   * @param {HeaderBag} [headers]
292
   * @param {int} [timeout]
293
   *
294
   * @return {HttpResponsePromise}
295
   */
296
}
297
298
BasicHttpClient.getDefaults = getDefaults
299
300
module.exports = {
301
  Client: BasicHttpClient,
302
  /** @deprecated */
303
  getDefaults: getDefaults
304
}
305